package org.mashart.pink.m.gps;

import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;
import java.util.Timer;
import java.util.TimerTask;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
import javax.bluetooth.DataElement;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import org.mashart.pink.m.gps.data.NMEAParser;
import org.mashart.pink.m.gps.data.NMEAAssembler;
import org.mashart.pink.m.gps.data.Talker;

/**
 * The only implementation of <code>DeviceConnector</code> interface
 * for the moment.
 *
 * Discovers BT devices and searches for GPS service
 * (poor implementation at the moment)
 */
public class BtConnector extends TimerTask implements DeviceConnector, DiscoveryListener {

    /**
     * <code>true</code> if this instance has been configured already
     * @see <code>configure()</code>
     */
    private boolean configured;
    /**
     * true - we got GPS data streaming via BT
     * false - something went wrong
     */
    private boolean openedStream;
    /**
     * Bluetooth stream connection with a GPS device
     */
    private StreamConnection con;
    /**
     * Input stream for GPS data
     */
    private InputStream in;
    /**
     * Keeps references of the discovered BT device to which we'll eventually
     * open the <code>StreamConnection</code>
     *
     * @see <code>deviceDiscovered()</code> implementation.
     */
    private RemoteDevice rd;
    private LocalDevice ld;
    private Vector remoteDevices;
    private Vector deviceNames;
    private DiscoveryAgent discoveryAgent;
    private String btConnectionURL;

    // obviously, 0x1101 is the UUID for
    // the Serial Profile
    UUID[] uuidSet = {new UUID(0x1101) };

    // 0x0100 is the attribute for the service name element
    // in the service record
    int[] attrSet = {0x0100};

    /**
     * Contains NMEA sentence string
     * Used in <code>getRaw()</code>
     */
    private byte[] buffer;
    /**
     * Our BT device address.
     * Configuration option
     * @see <code>configure()</code>
     */
    private String devAddress;
    /**
     * Stream connection port.
     * Configuration option
     * @see <code>configure()</code>
     */
    private int port;
    /**
     * Stream connection protocol
     * Configuration option
     * @see <code>configure()</code>
     */
    private String protocol;
    /**
     * true - takes over BT connection control
     * false - external GPS device is the master
     */
    private boolean master;
    /**
     * Threads synchronization
     */
    final private Object sem;
    final private Object sem2;

    /**
     * NMEA sentences parser
     */
    private Talker parser;

    /**
     * NMEA assembler
     */
    private NMEAAssembler assembler;

    /**
     * Milliseconds between two scans for NMEA sentence
     */
    private int interval;

    /**
     * switch, if true, the bluetooth connector is scanning for NMEA sentence and assembling waypoints
     */
    private boolean active;

    /**
     * status of bluetooth connector, if true, the scanning is finished and it is safe to close bluetooth connection
     */
    private boolean finished;

    /**
     * Timer
     */
    private Timer timer;

    /**
     * Constructor
     */
    public BtConnector() {
        configured = false;
        sem = new Object();
        sem2 = new Object();
        buffer = new byte[200];
        parser = new NMEAParser();
        assembler = new NMEAAssembler();
        interval = 100;
        remoteDevices = new Vector();
        deviceNames = new Vector();
        active = false;
        finished = true;
    }

    /*******************************************************
     * DeviceConnector interface methods
     */

    /**
     * Preparation for <code>initialize()</code>
     */
    public int configure() {
        configured = true;
        return 1;
    }
    /**
     * Initialize the input stream of BT device
     */
    public int discover(){
        int res;
        try{
            ld = LocalDevice.getLocalDevice();
            res = ld != null ? 1 : -1;
        }catch (BluetoothStateException e) {
            e.printStackTrace();
            res = -1;
        /* TODO better error handling, friendly error messages */
        }
        return res;
    }

    /**
     * Get the remote devices in vicinity
     */
    public void discoverRemoteDevices(){
        /* configure() method should be called first */
        if (!configured) {
            return;
        }

        // initialize
        remoteDevices.removeAllElements();
        deviceNames.removeAllElements();
        
        synchronized (sem) {
            try {
                discoveryAgent = ld.getDiscoveryAgent();
                /* initiate BT discovery */
                discoveryAgent.startInquiry( DiscoveryAgent.GIAC, this );
            } catch (BluetoothStateException e) {
                e.printStackTrace();
                return;
            /* TODO better error handling, friendly error messages */
            }
            try {
                /**
                 * Wait for the discovery to finish.
                 * We should have Stream connection opened passed this point */
                sem.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
                // J2ME threads can't be interrupted
            }
        }
    }

    /**
     * Get the names of discovered remote devices.
     * Should be called after <code>discoverRemoteDevices()</code>.
     * @return a vector containing the names of remote devices.
     * The order of names is the same as in vector <code>remoteDevices</code>.
     */
    public Vector getRemoteDeviceNames(){
        return deviceNames;
    }

    /**
     * Set the remote device to be used
     */
    public void setRemoteDevice(int selectedIndex){
        rd = (RemoteDevice) remoteDevices.elementAt(selectedIndex);
        synchronized (sem2) {
            try{
                discoveryAgent.searchServices(attrSet, uuidSet, rd , this);
            }catch(BluetoothStateException e){
                e.printStackTrace();
            }

            try {
                /**
                 * Wait for the discovery to finish.
                 * We should have Stream connection opened passed this point */
                sem2.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
                // J2ME threads can't be interrupted
            }
        }
    }

    /**
     * Get the bluetooth connection URL
     */
    public String getBtConnectoinURL(){
        return btConnectionURL;
    }

    /**
     * Set the bluetooth connection URL
     */
    public void setBtConnectoinURL(String url){
        btConnectionURL = url;
    }

    /**
     * Starts BT devices dicovery and should eventually establish
     * BT connection to the GPS device
     * @see <code>inquiryCompleted()</code>
     */
    public int initialize() {
        
        if (ld == null) {
            discover();
        }
            try {
//                con = (StreamConnection) Connector.open(protocol + "://" +
//                        devAddress + ":" + port + ";master=" + (master ? "true" : "false"));
                con = (StreamConnection) Connector.open(btConnectionURL);
                in = con.openInputStream();
                openedStream = true;
            } catch (IOException ex) {
            }

        active = false;
        finished = true;
        return openedStream ? 1 : -1 ;
    }

    /**
     * Activate
     */
    public int activate(){
        if(active){
            return 0;
        }
        active = true;
        timer = new Timer();
        timer.schedule(this, 0, interval);
        return 1;
    }

    /**
     * Deactivate
     */
    public int deactivate(){
        if(!active){
            return 0;
        }
        timer.cancel();
        waitTilFinish();
        active = false;
        return 1;
    }

    /**
     * @return NMEA sentence, e.g.
     * "$GPGGA,161229.487,3723.2475,N,12158.3416,W,1,07,1.0,9.0,M,,,,0000*18"
     */
    public String getRaw() {
        int len = 0;
        int chr;
        int fin = 0;

        try {
            while (fin < 2 && len < buffer.length) {
                /* read one character at a time */
                chr = in.read();
                switch (chr) {

                    /* Line Feed: NMEA sentence should end here */
                    case 10:
                        if (fin == 1) {
                            fin = 2;
                        }
                        break;

                    /* Carriage Return: we now need the last char, Line Feed */
                    case 13:
                        fin = 1;
                        break;

                    /* Blank space: we don't need that.
                     * shouldn't happen anyway */
                    case 32:
                        fin = 0;
                        break;

                    /* all the rest are useful characters */
                    default:
                        fin = 0;
                        buffer[len++] = (byte) chr;
                }
            }
        } catch (IOException ex) {
            // something went wrong
            return null;
        /* TODO error handling, e.g. notify user */
        }

        return new String(buffer, 0, len);
    }

    /**
     * @return a Waypoint
     */
    public Waypoint getWaypoint(){
        return assembler.get();
    }

    /**
     * Thread entry point. Receives the incoming NMEA sentences from bluetooth device and assembles the waypoint.
     */
    public void run(){
        finished = false;
        String sentence; // NMEA sentence

        // try to read raw NMEA sentence
        sentence = getRaw();

        // check if the sentence is valid
        if (sentence != null && parser.isValid(sentence)) {
            // push the NMEA sentence into the assembler
            assembler.put(sentence);
        }
        finished = true;
    }


    /**
     * Closes <code>InputStream</code> and <code>StreamConnection</code>
     */
    public int terminate() {
        if(active){
            // cancel scheduled scans
            deactivate();
        }
        // wait for the scanning thread to finish
        waitTilFinish();
        boolean term = true;
        try {
            in.close();
        } catch (IOException ex) {
            term = false;
        }
        try {
            con.close();
        } catch (IOException ex1) {
            term = false;
        }

        return term ? 1 : -1;
    }

    /**
     * Wait until the NMEA message scanning thread to finish
     */
    public void waitTilFinish(){
        while(!finished){
            try{
                Thread.sleep(interval);
            }catch(InterruptedException ex){
                // J2ME threads can't be interrupted
            }
        }
    }


    /*******************************************************
     * DiscoveryListener interface
     */

    /**
     * Callback for every BT device discovered during inquiry
     */
    public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
        remoteDevices.addElement(btDevice);
        String deviceName;
        try{
            deviceName = btDevice.getFriendlyName(false);
        }catch(IOException e){
            deviceName = "Unknown Device";
        }
        deviceNames.addElement(deviceName);
    }

    /**
     * Callback for every service discovered on a BT device
     * We don't use this for the moment.
     * TODO should we?
     */
    public void servicesDiscovered(int transID, ServiceRecord[] servRecord) {
            for(int i = 0; i < servRecord.length; i++) {
                DataElement serviceNameElement = servRecord[i].getAttributeValue(attrSet[0]);
                String _serviceName = (String)serviceNameElement.getValue();
                String serviceName = _serviceName.trim();
                btConnectionURL = servRecord[i].getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
            }
    }

    /**
     * Callback for completion of a BT services discovery
     * We don't use this for the moment.
     * TODO should we?
     */
    public void serviceSearchCompleted(int transID, int respCode) {
            if (respCode == DiscoveryListener.SERVICE_SEARCH_COMPLETED) {
                // the service search process was successful
                synchronized (sem2) {
                    // nofity sem.wait() we're done here
                    sem2.notify();
                }
            } else {
                // the service search process has failed
            }

    }

    /**
     * Callback for completion of <code>startInquiry()</code>
     * We should be ready to open a StreamConnection now
     */
    public void inquiryCompleted(int discType) {
        synchronized (sem) {
            // nofity sem.wait() we're done here
            sem.notify();
        }
    }
}